Generated code - Using the entity collection classes, Adapter
Preface
Adapter contains a general purpose EntityCollection class. This is different from SelfServicing where, per entity definition in a project,
LLBLGen Pro will generate an entity collection class. The EntityCollection class is located in the HelperClasses namespace in the
database generic project. This class is used to work on
more than one entity at the same time and it is used to retrieve more than one entity of the same type from the database.
This section describes the different kinds of functionality bundled in
the EntityCollection class, related to collection class and how to utilize that functionality in your code.
TwoClasses scenario
When you generate code using the TwoClasses preset, the entity collections will still be of type EntityCollection(Of
RelatedEntityType).
This is done to prevent compiler errors that EntityCollection(Of B) doesn't derive from EntityCollection(OF A) if B is a subtype of A. These errors can
occur because C# and VB.NET don't support a phenomenon called
co-variance which makes EntityCollection(Of B) a type which is castable to
EntityCollection(Of A) if B is a subtype of A.
The EntityCollection(Of T) class derives from the base class EntityCollectionBase2(Of T), which is a class in the ORMSupportClasses. The non-generic
EntityCollection class derives from the also non-generic class EntityCollectionNonGeneric. EntityCollectionNonGeneric is a class used for example for design time
databinding and for entitycollection usage behind the scenes. EntityCollectionNonGeneric derives from EntityCollectionBase2(Of EntityBase2).
Entity retrieval into an entity collection object
Entity collection objects can be filled with entities retrieved from the database using several ways. Below we'll walk you
through the ones you will use the most.
Using a related entity
The easiest way to retrieve a set of entities in an entity collection class, is by using a related entity, which in turn is used as
a filter. For example, let's use our user "CHOPS" again and let's retrieve all the order entities for that customer. Note that in the following example we do not
actually
fetch the customer entity from the database, we only use the object and its PK value to construct the filter.
// C#
CustomerEntity customer = new CustomerEntity("CHOPS");
DataAccessAdapter adapter = new DataAccessAdapter();
EntityCollection<OrderEntity> orders = customer.Orders;
adapter.FetchEntityCollection(orders, customer.GetRelationInfoOrders());
' VB.NET
Dim customer As New CustomerEntity("CHOPS")
Dim adapter As New DataAccessAdapter()
Dim orders As EntityCollection(Of OrderEntity)= customer.Orders
adapter.FetchEntityCollection(orders, customer.GetRelationInfoOrders())
The entity inside 'customer' is used to construct the filter bucket created by GetRelationInfoOrders() which filters the orders in the persistent storage on the
CustomerID field and value "CHOPS".
Adapter does not support lazy loading. All loading of data is by hand. This has the advantage that you can transfer an EntityCollection object to another
process/tier and be certain no database connection/logic is necessary or required to work with the data inside the collection. It also ensures no extra data
is available to the developer/object that you didn't supply. You can filter on more fields, including filtering on fields in different entities by adjusting
the RelationPredicateBucket object. The RelationPredicateBucket object is retrieved from the GetRelationInfo*() methods. You can also construct your
own if you want.
The EntityCollection object to fill and which is passed to the FetchEntityCollection() method has to contain a valid IEntityFactory2 implementing object. LLBLGen Pro will
generate such a factory for each entity. In the example above, customer.Orders is an EntityCollection instance created inside the customer object (and created by the
constructor of CustomerEntity) and already contains the valid factory object for OrderEntity objects.
If Order is in an inheritance hierarchy, the fetch is polymorphic. This means that if the customer entity, in this case customer "CHOPS", has references to
instances of different derived types of Order, every instance in customer.Orders is of the type it represents, which effectively means that
not every instance in Orders is of the same type. See for more information about polymorphic fetchs also
Polymorphic fetches.
Using a prefetch path
An easy way to retrieve a set of entities can be by using a Prefetch Path, to read related entities together with the entity or entities to fetch. See
for more information about Prefetch Paths and how to use them:
Prefetch Paths.
Using the collection object
The most flexible way to retrieve a set of entities in an entity collection is by simply using an instance of the EntityCollection object, create and fill a
new RelationPredicateBucket object (or use a retrieved one as a basis) and call a DataAccessAdapter object's FetchEntityCollection(). Most of the time you can start
with a bucket created by an entity instance' GetRelationInfo
FieldMappedOnRelationName method.
Let's concentrate on the EntityCollection that should contain / be filled with OrderEntity objects.
The entity order has a rich set of different relationships, with Customer, Employee and Shipper (m:1 relations), with OrderDetails
(1:n relation) and with Product (m:n relation over OrderDetail)
LLBLGen Pro will add GetRelationInfo*() methods to the OrderEntity class for each related entity to make life easier for you to create RelationPredicateBucket
objects to fetch collections of these related entities. Lets look at the two relation types which will end up in multiple entities being fetched:
1:n and m:n. 1:n is already addressed in the example above using a customer and its Orders collection. m:n relations are treated similarly.
Using m:n relations
When an object has one or more m:n (many to many) relationships with other entities, LLBLGen Pro will also generate easy to use
RelationPredicateBucket creation methods to filter objects for these kind of relations, using the related entity.
Per entity related with an m:n relation there's one GetRelationInfo
Field mapped on m:n relation method,
which returns a ready to use RelationPredicateBucket object.
Let's retrieve all orders which contain the purchase of a product X with productID 10. In the Order entity,
we named the field mapped on the m:n relation Product - Order 'Orders', which thus ends up in the method name: GetRelationInfoOrders(). We're not interested in
the product entity itself so that's not fetched. We pass the Orders collection directly without storing it into another reference variable.
// [C#]
ProductEntity product = new ProductEntity(10);
DataAccessAdapter adapter = new DataAccessAdapter();
adapter.FetchEntityCollection(product.Orders, product.GetRelationInfoOrders());
' [VB.NET]
Dim product As New ProductEntity(10)
Dim adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(product.Orders, product.GetRelationInfoOrders())
There are multiple ways to retrieve the same data in the framework LLBLGen Pro generates for you. It's up to you which one you'll
use in which situation.
Entity data manipulation using collection classes
Manipulating the entity data of more than one entity at once can be cumbersome when you work with objects that have to be
loaded into memory: all entities you want to manipulate have to be instantiated into memory, you have to alter the fields of
these objects and then save them individually. LLBLGen Pro offers functionality to work on entity data directly in the persistent
storage. This opens up the possibility to do bulk updates or bulk deletes with a single call to a method, greatly reducing the
database traffic and increasing performance. It also improves concurrency safety among threads, because you alter data directly
in the shared repository, so other threads will see changes immediately. See for an example of direct updating of entities
Using entity classes, Modifying an entity, option 3
Updating entities in a collection in memory
When you have loaded a set of entities in a collection and for example have bound this collection to a datagrid, the user probably
has altered one or more objects' fields in the collection. You can also alter the fields yourself by looping through the objects
inside the collection. When you want to save these changes to the persistent storage, you can use all save methods of the objects
inside the collection, but you can also use the SaveEntityCollection() method of the DataAccessAdapter object which walks all objects inside the
collection and, if the object is 'dirty', (which means, it's been changed and should be updated in the persistent storage) it is saved. This is all done in a
transaction if no transaction is currently available. (See for more information about transactions the section
Transactions).
Deleting one or more entities from the persistent storage
If you wish to delete one or more entities from the persistent storage, the same problem as with updating a set of entities
appears: you first have to load them into memory, call Delete() and they'll be deleted. To delete a set of entities from the
persistent storage, you can use the DeleteEntityCollection() method of the DataAccessAdapter object to achieve your goal.
This method works with the objects inside the collection and deletes them one by one from the persistent storage using
an own transaction if the current collection isn't part of an existing transaction. (See for more information about transactions the section
Transactions).
Client side sorting
The EntityCollection class doesn't implement IBindingList, as the EntityCollection class uses the
EntityView2 class
to bind to grids and other controls and also let these views do the filtering and sorting of the entity collection data. To keep backwards
compatibility, the Sort() methods of the EntityCollection class have been kept and work as they did in previous version of LLBLGen Pro.
It's recommended you use an EntityView2 class to sort and filter an entity collection instead of using the Sort() methods directly on an
entity collection. See for more information about EntityView2 classes:
Generated code - using entity views with entity collections.
To sort a fetched collection in memory, without going back to the database, use the entity collection method Sort (there are various overloads).
This method uses internally the ArrayList's QuickSort method on the property specified (either by field index or property name).
Two overloads also accept an IComparer object, which will then sort the entities based on the implementation of that IComparer object, which you
can supply yourself. Below is an example how to sort a fetched EntityCollection of customers in memory, on company name.
// C#
EntityCollection<CustomerEntity> customers =
new EntityCollection<CustomerEntity>(new CustomerEntityFactory());
DataAccessAdapter adapter = new DataAccessAdapter();
adapter.FetchEntityCollection(customers, null);
customers.Sort((int)CustomerFieldIndex.CompanyName, ListSortDirection.Descending);
' VB.NET
Dim customers As New EntityCollection(Of CustomerEntity)(New CustomerEntityFactory())
Dim adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(customers, Nothing)
customers.Sort(CType(CustomerFieldIndex.CompanyName, Integer), ListSortDirection.Descending)
Finding entities inside a fetched entity collection
Although it's recommended to use EntityView2 objects to filter and sort an in-memory EntityCollection object, it sometimes can be helpful to just
have a quick way to find in an in-memory entity collection an entity or group of entities matching a filter. The EntityCollection class offers this facility
through the method
FindMatches(IPredicate). FindMatches is a method which accepts a normal LLBLGen Pro predicate (see for more information about predicates
Generated code - getting started with filtering) and returns a list of indexes of all
entities matching that predicate.
As a PredicateExpression is also a predicate, you can specify a complex
filter, including filters on non-field properties, to find back the entities
you're looking for. FindMatches returns a List<int> / List(Of Integer).
The following example finds all indexes of customer entities from the UK in the fetched entity collection of customers. FindMatches will perform an in-memory
filter, it won't go to the database.
// C#
IPredicate filter = (CustomerFields.Country == "UK");
List<int> indexes = myCustomers.FindMatches(filter);
' VB.NET
Dim filter As IPredicate = (CustomerFields.Country = "UK")
Dim indexes As List(Of Integer) = myCustomers.FindMatches(filter)
Note:
|
When using FieldCompareValuePredicate with FindMatches, be sure to specify the value in the same type as the value of the field. For example, if the field is of type Int64, and you specify as value to compare the value 1, you'll be comparing an Int64 with an Int32, which will fail. Instead, specify the value, 1, as an Int64 as well.
|
FindMatches is the same routine which is also used by EntityView2 objects to find the entities which should belong in the view. As the routine is defined virtual /
Overridable, you can tweak the way the entities are matched.
Hierarchical projections of entity collections
LLBLGen Pro allows you to create projections of the full graph of all the entities inside a given entity collection onto a DataSet or a Dictionary object. A hierarchical projection is a projection where all entities in the entity collection plus all their related entities and so on, are grouped together per entity type. Say you have the following graph in memory: a set of CustomerEntity instances, which contain each a set of OrderEntity instances and each OrderEntity instance refers to an EmployeeEntity instance. This projection functionality is implemented on the entity collection, in the method
CreateHierarchicalProjection. It's implemented on the EntityCollection class and not on the EntityView2 class because it affects related entities as well, while EntityView is a view of 1-level deep on an entity collection.
With LLBLGen Pro it's possible to project this graph onto a DataSet which will result in per entity type a new DataTable
object with all instances of that entity type (and the data relations setup
correctly). You can also project it onto a Dictionary with per entity type an entity collection which contains the entities of that type. Projections are defined in instances of the
IViewProjectionData interface which is implemented in the
ViewProjectionData class. This class combines per type projections (as shown below in the example) which are then used as one projection on the complete graph.
By default, when projecting to a DataSet, only the entity types which have instances in the graph get a DataTable in the resulting DataSet. If you want to have a DataSet where there are always an expected number of DataTable instances (so for entities which aren't in the graph, they're empty), you can pre-create the DataSet and pass the pre-created DataSet to the projection routine. LLBLGen Pro's runtime library contains helper routines to produce an empty DataSet with empty DataTables, the correct columns and the proper DataRelation objects setup based on a prefetch path specified. Please consult the
LLBLGen Pro Reference Manual for the
GeneralUtils class'
ProduceEmptyDataSet and
ProduceEmptyDataTable routines.
Examples
The following examples will show you both projections (to DataSet and to Dictionary) of the earlier described graph of customers - Orders - Employees.
The examples will first fetch the complete graph of customers, orders and employees and will then create a projection of that graph. Usage of custom projections per property and
additional filters is also shown by the examples. Please refer to the
LLBLGen Pro reference manual for details about the generic ViewProjectionData class and its constructors.
Projection to DataSet
// C#
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, null, path);
}
// setup projections per type.
List<IEntityPropertyProjector> customerProjections = EntityFields2.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity));
// add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
// the value of the IsNew property of the customer entities.
customerProjections.Add(new EntityPropertyProjector(new EntityProperty("IsNew"), "IsNew"));
List<IEntityPropertyProjector> orderProjections = EntityFields2.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.OrderEntity));
List<IEntityPropertyProjector> employeeProjections = EntityFields.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.EmployeeEntity));
List<IViewProjectionData> projectionData = new List<IViewProjectionData>();
// create the customer projection information. Specify a filter so only customers from Germany
// are projected.
projectionData.Add(new ViewProjectionData<CustomerEntity>(
customerProjections, (CustomerFields.Country == "Germany"), true));
projectionData.Add(new ViewProjectionData<OrderEntity>(orderProjections, null, false));
projectionData.Add(new ViewProjectionData<EmployeeEntity>(employeeProjections));
DataSet result = new DataSet("projectionResult");
customers.CreateHierarchicalProjection(projectionData, result);
' VB.NET
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees)
Using adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(customers, Nothing, path)
End Using
' setup projections per type.
Dim customerProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity))
' add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
' the value of the IsNew property of the customer entities.
customerProjections.Add(New EntityPropertyProjector(New EntityProperty("IsNew"), "IsNew"))
Dim orderProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.OrderEntity))
Dim employeeProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.EmployeeEntity))
Dim projectionData As New List(Of IViewProjectionData)()
' create the customer projection information. Specify a filter so only customers from Germany
' are projected.
projectionData.Add(New ViewProjectionData(Of CustomerEntity)( _
customerProjections, (CustomerFields.Country = "Germany"), True))
projectionData.Add(New ViewProjectionData(Of OrderEntity)(orderProjections, Nothing, False))
projectionData.Add(New ViewProjectionData(Of EmployeeEntity)(employeeProjections))
Dim result As New DataSet("projectionResult")
customers.CreateHierarchicalProjection(projectionData, result)
The same projectors as used with the projection to the DataSet are usable with a projection to a Dictionary, which is almost equal to the DataSet example.
Projection to Dictionary
// C#
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers, null, path);
}
// setup projections per type.
List<IEntityPropertyProjector> customerProjections = EntityFields2.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity));
// add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
// the value of the IsNew property of the customer entities.
customerProjections.Add(new EntityPropertyProjector(new EntityProperty("IsNew"), "IsNew"));
List<IEntityPropertyProjector> orderProjections = EntityFields2.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.OrderEntity));
List<IEntityPropertyProjector> employeeProjections = EntityFields.ConvertToProjectors(
EntityFieldsFactory.CreateEntityFieldsObject(EntityType2.EmployeeEntity));
List<IViewProjectionData> projectionData = new List<IViewProjectionData>();
// create the customer projection information. Specify a filter so only customers from Germany
// are projected.
projectionData.Add(new ViewProjectionData<CustomerEntity>(
customerProjections, (CustomerFields.Country == "Germany"), true));
projectionData.Add(new ViewProjectionData<OrderEntity>(orderProjections, null, false));
projectionData.Add(new ViewProjectionData<EmployeeEntity>(employeeProjections));
Dictionary<Type, IEntityCollection> projectionResults = new Dictionary<Type, IEntityCollection>();
customers.CreateHierarchicalProjection(projectionData, projectionResults);
' VB.NET
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders).SubPath.Add(OrderEntity.PrefetchPathEmployees)
Using adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(customers, Nothing, path)
End Using
' setup projections per type.
Dim customerProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.CustomerEntity))
' add an additional projector so the destination DataTable will have an additional column called 'IsNew' with
' the value of the IsNew property of the customer entities.
customerProjections.Add(New EntityPropertyProjector(New EntityProperty("IsNew"), "IsNew"))
Dim orderProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.OrderEntity))
Dim employeeProjections As List(Of IEntityPropertyProjector) = EntityFields2.ConvertToProjectors( _
EntityFieldsFactory.CreateEntityFieldsObject(EntityType.EmployeeEntity))
Dim projectionData As New List(Of IViewProjectionData)()
' create the customer projection information. Specify a filter so only customers from Germany
' are projected.
projectionData.Add(New ViewProjectionData(Of CustomerEntity)( _
customerProjections, (CustomerFields.Country = "Germany"), True))
projectionData.Add(New ViewProjectionData(Of OrderEntity)(orderProjections, Nothing, False))
projectionData.Add(New ViewProjectionData(Of EmployeeEntity)(employeeProjections))
Dim projectionResults As New Dictionary(Of Type, IEntityCollection)()
customers.CreateHierarchicalProjection(projectionData, projectionResults)
Note: |
If you just want a structure with per entity type a collection with all the instances of that type in the entity graph, so not really a projection to new copies of the entities, please use the routine ObjectGraphUtils.ProduceCollectionsPerTypeFromGraph. The ObjectGraphUtils class is located in the ORMSupportClasses namespace and contains a variety of routines working on entity graphs. Please see the LLBLGen Pro reference manual for details on this class and this method. |
Tracking entity remove actions
Removing an entity from a collection by calling
entitycollection.
Remove(toRemove) or
entitycollection.
RemoveAt(index) is an ambiguous action: do you want to remove the entity from the collection to further process the entities left, or do you want to get rid of the entity completely, both in-memory and also in the database? This is the reason why LLBLGen Pro doesn't perform deletes on the database automatically if you remove an entity from a collection, you have to explicitly specify what entities to remove.
Tracking which entities are removed from an entity collection to be removed from the database can be a bit cumbersome if the collection is bound to a grid for example. To overcome this, LLBLGen Pro has a feature which makes an entity collection track the entities removed from it by using
another entity collection. This way, you can keep track of which entities are removed from the entity collection and pass them on to a
Unit of work object for persistence in one transaction together with the rest of the entities which have changed.
The extra collection is necessary because an entity which is removed from the collection isn't there anymore, so it can't be referred to by the collection itself. To enable removal tracking in an entity collection, set its
RemovedEntitiesTracker property to a collection into which you want to track the removed entities from the collection. This collection can then be added to a UnitOfWork2 object for deletion by using the method
unitofwork2.
AddCollectionForDelete(collectionWithEntitiesToDelete) or you can delete the entities by calling
DataAccessAdapter.
DeleteEntityCollection(collectionWithEntitiesToDelete).
The following example illustrates this.
// C#
// First fetch all customers from Germany with their orders.
EntityCollection<CustomerEntity> customers = new EntityCollection<CustomerEntity>();
PrefetchPath2 path = new PrefetchPath2(EntityType.CustomerEntity);
path.Add(CustomerEntity.PrefetchPathOrders);
using(DataAccessAdapter adapter = new DataAccessAdapter())
{
adapter.FetchEntityCollection(customers,
new RelationPredicateBucket(CustomerFields.Country == "Germany"),
path);
}
// we now will add a tracker collection to the orders collection of customer 0.
EntityCollection<OrderEntity> tracker = new EntityCollection<OrderEntity>();
customers[0].Orders.RemovedEntitiesTracker = tracker;
// after this, we can do this:
customers[0].Orders.Remove(myOrder);
// and myOrder is removed from the in-memory collection customers[0].Orders
// and it's placed in tracker. We can now delete the entities in tracker
// by using a UnitOfWork2 object or by calling adapter.DeleteEntityCollection(tracker).
' VB.NET
' First fetch all customers from Germany with their orders.
Dim customers As New EntityCollection(Of CustomerEntity)()
Dim path As New PrefetchPath2(EntityType.CustomerEntity)
path.Add(CustomerEntity.PrefetchPathOrders)
Using adapter As New DataAccessAdapter()
adapter.FetchEntityCollection(customers, _
new RelationPredicateBucket(CustomerFields.Country = "Germany"), _
path)
End Using
' we now will add a tracker collection to the orders collection of customer 0.
Dim tracker As New EntityCollection(Of OrderEntity)()
customers(0).Orders.RemovedEntitiesTracker = tracker
' after this, we can do this:
customers(0).Orders.Remove(myOrder)
' and myOrder is removed from the in-memory collection customers[0].Orders
' and it's placed in tracker. We can now delete the entities in tracker
' by using a UnitOfWork2 object or by calling adapter.DeleteEntityCollection(tracker).
Note: |
Tracking removal of an entity isn't used by the Clear() method, because Clear is often used to clean up a collection and not to remove entities from the database, so to avoid false positives and the deletion of entities which weren't suppose to be deleted, removal tracking isn't available for the Clear method. |